2019年6月25日
		      
                      
                          
                          Jerry
                      
                      
                          
                          1621
                      
                      
                          
                          2019年6月27日
                  
              人生不止眼前的苟且,代码也不止数据的增删改查,也有有趣的网络编程。如何用C语言做一个简单的服务器和客户端,实现一个聊天室程序呢?这里就简单的写一下博主的实现。
一、程序需求
实现一个简单的服务器,包括以下功能:
- 可以监听并且与多个客户端建立TCP链接。
 - 可以接收客户端发来的消息,并发送给所有的客户端。
 - 可查询所有的客户端链接。
 
实现客户端有如下功能:
- 输入服务器端口号,与服务器端建立链接。
 - 发送消息给服务器。
 - 接收并打印服务器发来的消息。
 
二、实现分析
借用百度的一张图,这个流程很好的解释了socket编程的根本:

服务器端的处理流程思路:
- 创建socket套接字
 - 绑定ip与端口号
 - 创建监听线程,等待客户端链接
 - 接收到一个客户端链接则创建一个线程进行消息接收处理
 - 客户端关闭,关闭相关进程
 - 服务器关闭
 
客户端的处理流程思路:
- 创建socket套接字
 - 输入端口号链接服务器
 - 创建线程处理服务器消息
 - 接收控制台输入发送给服务器
 - 客户端关闭
 
socket的基本api:
包含在在头文件“winsock2.h”,官网说明地址: https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/
三、基本的socket API
1、创建一个socket套接字:
/*
domain:协议域、地址域或协议族。常用的协议族有:AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX)、AF_ROUTE等等
type:socket类型。常用的socket类型有:SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等
protocol:指定使用的协议。常用的协议有:IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等等
返回值:成功返回一个非负整数的fd,失败则返回-1
*/
int socket(int domain, int type, int protocol);
2、绑定套接字
/*
sockfd:socket文件描述符,即socket()的返回值
addr:指向sock地址的指针
addrlen:sock地址的长度
返回值:成功返回0,失败返回-1
*/
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
3、监听套接字
/*
sockfd:被监听的套接字
backlog:最大等待队列数量
返回值:成功返回0,失败返回-1
*/
int listen(int sockfd, int backlog);
4、连接socket套接字
/*
sockfd:客户端的套接字
addr:服务器的地址
addrlen:服务器的地址的长度
返回值:成功返回0,失败返回-1
*/
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
5、接受socket连接请求
/*
sockfd:被监听的套接字
addr:返回客户端的地址,可为NULL
addrlen:指定客户端的地址的长度,可为NULL
返回值:成功返回已连接的新套接字描述符connfd,失败返回-1
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
6、socket接收数据
/*
sockfd:从这个socket接收数据
buf:用来保存接收到的数据
len:指定buf的长度,表示最多接收这么多个字节的数据
flags:flags,指定对应的选项,一般置为0
返回值:成功返回接收到的数据大小,返回0表示对方不再发送数据(可以理解为关闭了连接),出错返回-1
*/
int recv(int sockfd, void *buf, int len, int flags);
7、socket发送数据
/*
sockfd:向这个socket发送数据
buf:发送buf指向的数据
len:指定buf的长度,指定要发送的数据大小
flags:flags,指定对应的选项,一般置为0
返回值:成功返回已发送的数据大小,失败则返回-1
*/
int send(int sockfd, void *buf, int len, int flags);
8、从udp socket接收数据
/*
sockfd:输入参数,从该socket接收数据
buf:输出参数,将接收的数据存放在buf上
len:输入参数,指定buf的长度
flags:输入参数,flags,指定对应的选项,一般置为0
addr:输出参数,保存该数据的发送方地址
addrlen:输入参数,指定发送方地址的长度
返回值:成功返回接收到的数据大小,失败则返回-1
*/
int recvfrom(int sockfd, void *buf, int len, int flags, struct sockaddr *addr, socklen_t *addrlen);
9、向udp socket发送数据
/*
sockfd:输入参数,向该socket发送数据
buf:输入参数,发送buf指向的数据
len:输入参数,指定buf的长度
flags:输入参数,flags,指定对应的选项,一般置为0
addr:输入参数,指定接收方的地址
addrlen:输入参数,指定接收方的地址的长度
返回值:成功返回发送的数据大小,失败则返回-1
*/
int sendto(int sockfd, void *buf, int len, int flags, struct sockaddr *addr, socklen_t addrlen);
10、关闭socket
/*
fd: 要关闭的socketID
*/
int close(int fd);
四、最终实现效果
服务器端:

客户端:

1、建立多个链接

2、发送消息

3、客户端断开链接

4、服务器关闭

五、代码实现
服务器端:
// server.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")
#define MAX_NUM 10
typedef struct _client
{
	int port;
	SOCKET socketid;
	HANDLE rev_handle;
}CLIENT_S;
//全局变量区
HANDLE acpt_handle;
CLIENT_S g_client[MAX_NUM];
int g_num = 0;
void init_client()
{
	int i;
	for(i=0; i < MAX_NUM; i++)
	{
		memset(&g_client[i], 0, sizeof(CLIENT_S));
	}
	g_num = 0;
}
void log_print(char *str)
{
	printf("[系统日志]:%s\n", str);
}
void message_print(char *str, int port)
{
	printf("[用户%d]:%s\n", port, str);
}
void del(int index)
{
	int i;
	for(i=index; i< g_num; i++)
	{
		memcpy(&g_client[i], &g_client[i+1], sizeof(CLIENT_S));
	}
	memset(&g_client[g_num-1], 0,  sizeof(CLIENT_S));
	g_num--;
}
int getindex_bysocket(SOCKET s)
{
	int i;
	for(i=0;i<g_num;i++)
	{
		if(g_client[i].socketid == s)
		{
			return i;
		}
	}
	return -1;
}
void send_all(char * sendData)
{
	int i;
	for(i=0;i<g_num;i++)
	{
		send(g_client[i].socketid, sendData, (int)strlen(sendData), 0);
	}
}
int MyRevThread(LPVOID lpParam)
{
    SOCKET sClient = SOCKET(lpParam);
	char revData[255]; 
	char sendData[255];
	int keeplive = true;
	while(keeplive)
	{
		//接收数据
        int ret = recv(sClient, revData, 255, 0);
		//printf("Myret: %d\n",ret);
        if(ret > 0)
        {
            revData[ret] = 0x00;
			int index;
			index = getindex_bysocket(sClient);
			sprintf(sendData, "[用户%d]: %s", g_client[index].port, revData);
			send_all(sendData);
			message_print(revData, g_client[index].port);
        }
		else if(ret == -1)
		{
			int index;
			index = getindex_bysocket(sClient);
			char log[50];
			sprintf(log, "[用户%d] 断开链接!", g_client[index].port);
			log_print(log);
			closesocket(g_client[index].socketid);
			CloseHandle(g_client[index].rev_handle);
			keeplive = false;
			del(index);
			break;
		}
		
	}
    return 0;
}
int MyAcpThread(LPVOID lpParam)
{
	SOCKET slisten = SOCKET(lpParam);
	while(true)
	{
		SOCKET sClient;
		sockaddr_in remoteAddr;
		int nAddrlen = sizeof(remoteAddr);
		sClient = accept(slisten, (SOCKADDR *)&remoteAddr, &nAddrlen);
		if(sClient == INVALID_SOCKET)
		{
			log_print("接受到一个连接失败...!");
			return -1;
		}
		
		DWORD thID=0;
		HANDLE handle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyRevThread, (LPVOID)sClient, 0, (LPDWORD)&thID);
		if(handle == NULL)
		{
			log_print("消息接收线程创建失败!");
			return -1;
		}
		char log[50];
		sprintf(log, "[用户%d] 加入成功! 线程ID %d", remoteAddr.sin_port, thID);
		log_print(log);
		g_client[g_num].port = remoteAddr.sin_port;
		g_client[g_num].rev_handle = handle;
		g_client[g_num].socketid = sClient;
		g_num++;
	}
    return 0;
}
void start()
{
	//初始化WSA
    WORD sockVersion = MAKEWORD(2,2);
    WSADATA wsaData;
    if(WSAStartup(sockVersion, &wsaData)!=0)
    {
        log_print("初始化WSA失败!");
		return;
    }
	log_print("初始化WSA完成!");
	//创建套接字
    SOCKET slisten = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(slisten == INVALID_SOCKET)
    {
		log_print("创建套接字失败!");
        return;
    }
	log_print("创建套接字完成!");
	//绑定IP和端口
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(8888);;
    sin.sin_addr.S_un.S_addr = INADDR_ANY; 
    if(bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
    {
		log_print("创建套接字失败!");
		return;
    }
	log_print("绑定IP和端口完成!");
	//开始监听
    if(listen(slisten, 5) == SOCKET_ERROR)
    {
		log_print("开始监听失败!");
        return ;
    }
	HANDLE handle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyAcpThread, (LPVOID)slisten, 0, NULL);
	if(handle == NULL)
	{
		log_print("客户端监听线程创建失败!");
		return ;
	}
	acpt_handle = handle;
	log_print("开始监听...等待连接!");
    //closesocket(slisten);
    //WSACleanup();
}
void print_menu()
{
	printf("************************************\n");
	printf("*****************欢迎***************\n");
	printf("************************************\n\n");
	printf("************ 1、查看所有客户端********\n");
	printf("************ 0、关闭服务器并退出*****\n");
	printf("************************************\n");
	
}
void print_client(int index)
{
	printf("************************************\n");
	printf("索引:%d\t", index);
	printf("端口号:%d\t\n", g_client[index].port);
	printf("************************************\n");
}
void print_all()
{
	int optVal;
	int optLen = sizeof(int);
	int i;
	for(i=0;i<g_num;i++)
	{
		print_client(i);
	}
}
int _tmain(int argc, _TCHAR* argv[])
{
	unsigned int ch;
	init_client();
	start();
	while(true)
	{
		print_menu();
		scanf("%u", &ch);
		getchar();
		switch(ch)
		{
			case 0:
				send_all("[系统消息]:服务器关闭,再见!");
				printf("退出!\n");
                WSACleanup();
				exit(0);
				break;
			case 1:
				print_all();
				break;
			default:
				printf("输入错误,重新选择!\n");
				break;
		}
	}
	return 0;
}
客户端:
// client.cpp
#include <WINSOCK2.H>
#include <STDIO.H>
 
#pragma  comment(lib,"ws2_32.lib")
 
int MyRevThread(LPVOID lpParam)
{
    SOCKET sClient = SOCKET(lpParam);
	char revData[255]; 
	int keeplive = true;
	while(keeplive)
	{
		//接收数据
        int ret = recv(sClient, revData, 255, 0);
        if(ret > 0)
        {
            revData[ret] = 0x00;
			printf("\n收到一条消息:\n");
			printf(revData);
			printf("\n");
        }
		else if(ret == -1)
		{
			char * log="";
			closesocket(sClient);
			keeplive = false;
			break;
		}
		
	}
    return 0;
}
int main(int argc, char* argv[])
{
	unsigned int port = 0;
	printf("输入服务器端口号:");
	scanf("%u", &port);
    WORD sockVersion = MAKEWORD(2,2);
    WSADATA data; 
    if(WSAStartup(sockVersion, &data) != 0)
    {
        return 0;
    }
 
    SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(sclient == INVALID_SOCKET)
    {
        printf("invalid socket !");
        return 0;
    }
 
    sockaddr_in serAddr;
    serAddr.sin_family = AF_INET;
    serAddr.sin_port = htons(port);
    serAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); 
    if (connect(sclient, (sockaddr *)&serAddr, sizeof(serAddr)) == SOCKET_ERROR)
    {
        printf("connect error !");
        closesocket(sclient);
        return 0;
    }
	HANDLE handle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyRevThread, (LPVOID)sclient, 0, NULL);
	if(handle == NULL)
	{
		printf("消息接收线程创建失败!");
		return -1;
	}
	printf("连接服务器成功!\n");
	while(1)
	{
		char input[30]="";
		scanf("%s", &input);
		char * sendData = input;
		send(sclient, sendData, strlen(sendData), 0);
	}
    closesocket(sclient);
    WSACleanup();
    return 0;
}
搞起!
                  
                  原创文章,转载请注明出处:
                  https://jerrycoding.com/article/socket-chat
              
              
 
	      
              
          微信
          
        
        
        
          支付宝
          
        
      
        